package com.key.w8.http1.handler.codec;

import com.key.w8.http1.connection.Connection;
import com.key.w8.http1.entity.HttpObject;
import com.key.w8.http1.entity.HttpRequest;
import com.key.w8.http1.entity.HttpResponse;
import com.key.w8.http1.exception.RequestException;
import com.key.w8.http1.exception.ResponseException;
import com.key.w8.http1.factory.FileFiled;
import com.key.w8.http1.factory.FormData;
import io.netty.buffer.ByteBuf;
import io.netty.buffer.ByteBufAllocator;
import io.netty.buffer.Unpooled;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelPromise;
import io.netty.handler.codec.http.*;
import io.netty.handler.codec.http.multipart.HttpPostRequestEncoder;
import io.netty.handler.codec.http.multipart.MemoryFileUpload;
import lombok.extern.slf4j.Slf4j;

import java.nio.charset.StandardCharsets;
import java.util.List;
import java.util.Map;

import static com.key.w8.http1.constant.Constants.*;

/**
 * codec即保证了部分的简单编解码，又相当于一个pipeline到connection之间的转发器，
 * connection依赖于codec的写能力，而codec又需要把读到的数据转发给connection
 * @Author: k8
 * @CreateTime: 2025-02-21
 * @Version: 1.0
 * 提供简单的Http请求的发送功能，生成FullHttpRequest
 * 为Connection赋能
 */
@Slf4j
public abstract class Http1Codec implements ByteBufAllocatorHolder{

    /**
     * 该ctx为某个handler的ctx，最好为最后一个，如{@link com.key.w8.http1.handler.Http1FullHandler}
     */
    private ChannelHandlerContext tailCtx;
    private HttpObjectServerResolver httpObjectServerResolver;
    private HttpObjectClientResolver httpObjectClientResolver;
    @Override
    public ByteBufAllocator alloc(){
        return tailCtx.alloc();
    }
    public void setTailChannelHandlerContext(ChannelHandlerContext tailCtx) {
        this.tailCtx = tailCtx;
    }

    public void ping(){
        FullHttpRequest ping = new DefaultFullHttpRequest(HttpVersion.HTTP_1_1, HttpMethod.POST,"");
        HttpHeaders headers = ping.headers();
        headers.set(PING_HEADER_NAME,1);
        headers.set(Time_Header,System.currentTimeMillis());
        tailCtx.writeAndFlush(ping);
    }
    public void pong(){
        FullHttpResponse httpResponse = new DefaultFullHttpResponse(HttpVersion.HTTP_1_1,HttpResponseStatus.NO_CONTENT);
        httpResponse.headers()
                .set(PONG_HEADER_NAME,1);
        tailCtx.writeAndFlush(httpResponse);
    }
    public ChannelPromise getPromise(){
        return tailCtx.newPromise();
    }

    public void writeRequestContent(byte[] data,String uri){
        writeRequestContent(tailCtx.voidPromise(),data,uri);
    }
    public void writeRequestContent(byte[] data,String uri,ChannelPromise promise){

        writeRequestContent(promise,data,uri);
    }
    /**
     *
     * @param data
     * @param uri
     * @param promise {@link Http1Codec#getPromise()}
     */
    public void writeRequestContent(ChannelPromise promise,byte[] data,String uri){
        ByteBuf content = tailCtx.alloc().buffer(data.length);
        content.writeBytes(data);
        FullHttpRequest httpRequest =
                new DefaultFullHttpRequest(HttpVersion.HTTP_1_1,HttpMethod.POST,uri,content);
        httpRequest.headers()
                .set(HttpHeaderNames.CONTENT_TYPE, "application/octet-stream")
                .set(HttpHeaderNames.CONTENT_LENGTH,content.readableBytes());
        tailCtx.writeAndFlush(httpRequest,promise);
    }
    public void writeRequest(HttpRequest request){
        writeRequest(tailCtx.voidPromise(),request);
    }
    public void writeRequest(ChannelPromise promise, HttpRequest request){
        io.netty.handler.codec.http.HttpRequest fRequest = null;
        HttpObject.DataContentType dataContentType = request.contentType();
        switch (dataContentType) {
            case BUFFER:
            {
                ByteBuf content = tailCtx.alloc().buffer(request.buffer().remaining()).writeBytes(request.buffer());
                fRequest = new DefaultFullHttpRequest(request.version(), request.method(), request.uri(), content, request.headers(), EmptyHttpHeaders.INSTANCE);
                fRequest.headers()
                        .set(HttpHeaderNames.CONTENT_LENGTH, content.readableBytes());
                if (!fRequest.headers().contains(HttpHeaderNames.CONTENT_TYPE)){
                    fRequest.headers()
                        .set(HttpHeaderNames.CONTENT_TYPE, BYTES_CONTENT_TYPE);
                }
                writeFullRequestAndFlush(fRequest,request,promise);
                break;
            }
            case DATA:
            {
                ByteBuf content = tailCtx.alloc().buffer(request.data().length).writeBytes(request.data());
                fRequest = new DefaultFullHttpRequest(request.version(), request.method(), request.uri(), content, request.headers(), EmptyHttpHeaders.INSTANCE);
                fRequest.headers()
                        .set(HttpHeaderNames.CONTENT_LENGTH, content.readableBytes());
                if (!fRequest.headers().contains(HttpHeaderNames.CONTENT_TYPE)){
                    fRequest.headers()
                            .set(HttpHeaderNames.CONTENT_TYPE, BYTES_CONTENT_TYPE);
                }
                writeFullRequestAndFlush(fRequest,request,promise);
                break;
            }
            case MESSAGE:
            {
                byte[] bytes = request.message().getBytes(StandardCharsets.UTF_8);
                ByteBuf content = tailCtx.alloc().buffer(bytes.length).writeBytes(bytes);
                fRequest = new DefaultFullHttpRequest(request.version(), request.method(), request.uri(), content, request.headers(), EmptyHttpHeaders.INSTANCE);
                fRequest.headers()
                        .set(HttpHeaderNames.CONTENT_LENGTH, content.readableBytes());
                if (!fRequest.headers().contains(HttpHeaderNames.CONTENT_TYPE)){
                    fRequest.headers()
                            .set(HttpHeaderNames.CONTENT_TYPE, STRING_CONTENT_TYPE_PREFIX + DEFAULT_CHARSET);
                }
                writeFullRequestAndFlush(fRequest,request,promise);
                break;
            }
            case BUF:
            {
                fRequest = new DefaultFullHttpRequest(request.version(), request.method(), request.uri(), request.content(), request.headers(), EmptyHttpHeaders.INSTANCE);
                fRequest.headers()
                        .set(HttpHeaderNames.CONTENT_LENGTH, request.content().readableBytes());
                if (!fRequest.headers().contains(HttpHeaderNames.CONTENT_TYPE)){
                    fRequest.headers()
                            .set(HttpHeaderNames.CONTENT_TYPE, BYTES_CONTENT_TYPE);
                }
                writeFullRequestAndFlush(fRequest,request,promise);
                break;
            }
            case COMPLEX_OBJECT_FROM_READ:
            {
                fRequest = httpObjectClientResolver.resolverRequestOut(request, this);
                if (fRequest == null) {
                    throw new RequestException("Consumer httpObjectClientResolver set error, can not resolver this out request.");
                }
                writeFullRequestAndFlush(fRequest,request,promise);
                break;
            }
            case FORM_DATA:{
                DefaultHttpRequest formDataRequest = new DefaultHttpRequest(request.version(), request.method(), request.uri(), request.headers());
                FormData formData = request.formData();
                try {
                    writeFormData(formDataRequest,request,formData,tailCtx,promise);
                } catch (Exception e) {
                    throw new RuntimeException(e);
                }
                break;
            }
            default:
            {
                fRequest = new DefaultFullHttpRequest(request.version(), request.method(), request.uri(), Unpooled.buffer(0), request.headers(), EmptyHttpHeaders.INSTANCE);
                fRequest.headers().set(HttpHeaderNames.CONTENT_LENGTH, 0);
                writeFullRequestAndFlush(fRequest,request,promise);
                break;
            }
        }

    }
    private void writeFullRequestAndFlush(io.netty.handler.codec.http.HttpRequest fRequest, HttpRequest request, ChannelPromise promise){
        String originalUri = fRequest.uri();
        Map<String, List<String>> parameters = request.getUrlParameters();
        if (parameters != null){
            fRequest.setUri(addParametersToUri(originalUri,parameters));
        }
        //这里最终会被提交给eventLoop进行异步执行，所以抛出异常并不能阻塞connection的runOut
        tailCtx.writeAndFlush(fRequest,promise);
    }
    private void writeFormData(DefaultHttpRequest fRequest, HttpRequest request, FormData formData, ChannelHandlerContext tailCtx, ChannelPromise promise) throws Exception {
        String originalUri = fRequest.uri();
        Map<String, List<String>> parameters = request.getUrlParameters();
        if (parameters != null){
            fRequest.setUri(addParametersToUri(originalUri,parameters));
        }
        HttpPostRequestEncoder encoder = new HttpPostRequestEncoder(fRequest,true);
        Map<String, String> formFiledMap = formData.getFormFiledMap();
        if (formFiledMap != null && !formFiledMap.isEmpty()){
            for (String name : formFiledMap.keySet()) {
                String value = formFiledMap.get(name);
                encoder.addBodyAttribute(name,value);
            }
        }

        Map<String, FileFiled> formFileFiledMap = formData.getFormFileFiledMap();
        if (formFileFiledMap != null && !formFileFiledMap.isEmpty()){
            for (String name : formFileFiledMap.keySet()) {
                FileFiled value = formFileFiledMap.get(name);
                if (value.getFile() != null){
                    encoder.addBodyFileUpload(value.getName(),value.getFilename(),value.getFile(),value.getContentType(),value.isText());
                }else if (value.getContent() != null){
                    byte[] content = value.getContent();
                    MemoryFileUpload memoryFileUpload = new MemoryFileUpload(name, value.getFilename(), value.getContentType(), null, HttpConstants.DEFAULT_CHARSET,content.length);
                    ByteBuf buf = alloc().buffer(content.length);
                    buf.writeBytes(content);
                    memoryFileUpload.setContent(buf);
                    encoder.addBodyHttpData(memoryFileUpload);
                }else {
                    throw new IllegalStateException("File and content is null in FileFiled.");
                }
            }
        }
        try{
            io.netty.handler.codec.http.HttpRequest httpRequest = encoder.finalizeRequest();
            tailCtx.write(httpRequest);
            while (encoder.isChunked()) {
                //这个alloc没啥用，并没有使用
                HttpContent chunk = encoder.readChunk(tailCtx.alloc());
                if (chunk == null) break;
                if (chunk instanceof LastHttpContent) {
                    tailCtx.writeAndFlush(chunk).addListener(f -> {
                        encoder.cleanFiles();
                        promise.trySuccess();
                    });
                } else {
                    tailCtx.write(chunk);
                }
            }
        }catch (Exception e){
            encoder.cleanFiles();
            promise.setFailure(e);
            throw e;
        }
    }
    /*private void writeFormData(DefaultHttpRequest fRequest, HttpRequest request, FormData formData, ChannelHandlerContext tailCtx, ChannelPromise promise) throws Exception {
        String originalUri = fRequest.uri();
        Map<String, List<String>> parameters = request.getUrlParameters();
        if (parameters != null){
            fRequest.setUri(addParametersToUri(originalUri,parameters));
        }
        tailCtx.write(fRequest);
        ByteBuf buffer = null;
        String boundary = formData.getBoundary();
        String tail = "\r\n--" + boundary + "--\r\n";
        byte[] tailBytes = tail.getBytes(StandardCharsets.UTF_8);
        Map<String, String> formFiledMap = formData.getFormFiledMap();
        if (formFiledMap != null && !formFiledMap.isEmpty()){
            for (String name : formFiledMap.keySet()) {
                String value = formFiledMap.get(name);
                if (buffer == null) {
                    buffer = tailCtx.alloc().buffer();
                }
                String type = "text/plain";
                String format = String.format("--%s\r\nContent-Disposition: form-data; name=\"%s\"\r\nContent-Type: %s; charset=UTF-8" +
                        "\r\n\r\n%s\r\n", boundary, name, type, value);
                buffer.writeBytes(format.getBytes(StandardCharsets.UTF_8));
                *//*buffer.writeBytes(("--" + boundary + "\r\n").getBytes());
                buffer.writeBytes(("Content-Disposition: form-data; name=\"" + name + "\"\r\n").getBytes());
                buffer.writeBytes("Content-Type: text/plain; charset=UTF-8\r\n\r\n".getBytes());
                buffer.writeBytes((value + "\r\n").getBytes(StandardCharsets.UTF_8));*//*
            }
        }
        if (buffer != null){
            tailCtx.write(new DefaultHttpContent(buffer));
        }
        Map<String, File> formFileFiledMap = formData.getFormFileFiledMap();
        String pre = "";
        if (formFileFiledMap != null && !formFileFiledMap.isEmpty()){
            for (String name : formFileFiledMap.keySet()) {
                String type = "application/octet-stream";
                String format = pre + String.format("--%s\r\nContent-Disposition: form-data; name=\"%s\"\r\nContent-Type: %s; charset=UTF-8" +
                        "\r\n\r\n", boundary, name, type);
                byte[] bytes =  format.getBytes(StandardCharsets.UTF_8);
                ByteBuf buf = tailCtx.alloc().buffer(bytes.length);
                buf.writeBytes(bytes);
                tailCtx.write(new DefaultHttpContent(buf));
                FileChannel channel = formFileFiledMap.get(name);
                ChunkedNioFile chunkedNioFile = new ChunkedNioFile(channel);
                HttpChunkedInput httpChunkedInput = new HttpChunkedInput(chunkedNioFile);
                *//*HttpContent content = null;
                while ((content = httpChunkedInput.readChunk(tailCtx.alloc())) != null){
                    tailCtx.write(content);
                }*//*
                tailCtx.write(httpChunkedInput);
                pre = "\r\n";
            }
        }
        ByteBuf buf = tailCtx.alloc().buffer(tailBytes.length);
        buf.writeBytes(tailBytes);
        tailCtx.writeAndFlush(new DefaultLastHttpContent(buf)).addListener(future -> {
            if (future.isSuccess()){
                promise.setSuccess();
            } else{
                promise.setFailure(future.cause());
            }
            if (formFileFiledMap != null) {
                for (FileChannel channel : formFileFiledMap.values()) {
                    try {
                        channel.close();
                    } catch (IOException e) {
                        log.debug("Http1Codec:File Channel close error.");
                    }
                }
            }
        });
    }*/


    private static String addParametersToUri(String originalUri, Map<String, List<String>> parameters) {
        String queryString = buildQueryString(parameters);
        if (queryString.isEmpty()) {
            return originalUri;
        }

        String separator = originalUri.contains("?") ? "" : "?";
        return originalUri + separator + queryString;
    }

    private static String buildQueryString(Map<String, List<String>> parameters) {
        StringBuilder query = new StringBuilder();
        for (Map.Entry<String, List<String>> entry : parameters.entrySet()) {
            String key = entry.getKey();
            List<String> values = entry.getValue();
            if (values == null || values.isEmpty()) {
                continue; // 忽略空值
            }
            for (String value : values) {
                value = value == null ? "" : value;
                query.append(key).append("=").append(value).append("&");
            }
        }
        if (query.length() > 0){
            query.deleteCharAt(query.length()-1);
        }
        return query.toString();
    }

    public void writeResponseContent(ChannelPromise promise,byte[] data){
        ByteBuf content = tailCtx.alloc().buffer(data.length);
        content.writeBytes(data);
        DefaultFullHttpResponse response = new DefaultFullHttpResponse(HttpVersion.HTTP_1_1,HttpResponseStatus.OK,content);
        response.headers()
                .set(HttpHeaderNames.CONTENT_TYPE, "application/octet-stream")
                .set(HttpHeaderNames.CONTENT_LENGTH,content.readableBytes());
        tailCtx.writeAndFlush(response,promise);
    }
    public void writeResponseContent(byte[] data){
        writeResponseContent(tailCtx.voidPromise(),data);
    }
    public ChannelPromise writeResponse(HttpResponse response){
        return writeResponse(tailCtx.voidPromise(),response);
    }
    public ChannelPromise writeResponse(ChannelPromise promise, HttpResponse response){
        FullHttpResponse fullHttpResponse;
        HttpObject.DataContentType dataContentType = response.contentType();
        switch (dataContentType) {
            case BUFFER:
            {
                ByteBuf content = tailCtx.alloc().buffer(response.buffer().remaining()).writeBytes(response.buffer());
                fullHttpResponse = new DefaultFullHttpResponse(response.version(), response.status(), content, response.headers(), EmptyHttpHeaders.INSTANCE);
                fullHttpResponse.headers()
                        .set(HttpHeaderNames.CONTENT_LENGTH, content.readableBytes());
                if (!fullHttpResponse.headers().contains(HttpHeaderNames.CONTENT_TYPE)){
                    fullHttpResponse.headers()
                            .set(HttpHeaderNames.CONTENT_TYPE, BYTES_CONTENT_TYPE);
                }
                break;
            }
            case DATA:
            {
                ByteBuf content = tailCtx.alloc().buffer(response.data().length).writeBytes(response.data());
                fullHttpResponse = new DefaultFullHttpResponse(response.version(), response.status(), content, response.headers(), EmptyHttpHeaders.INSTANCE);
                fullHttpResponse.headers()
                        .set(HttpHeaderNames.CONTENT_LENGTH, content.readableBytes());
                if (!fullHttpResponse.headers().contains(HttpHeaderNames.CONTENT_TYPE)){
                    fullHttpResponse.headers()
                            .set(HttpHeaderNames.CONTENT_TYPE, BYTES_CONTENT_TYPE);
                }
                break;
            }
            case MESSAGE:
            {
                byte[] bytes = response.message().getBytes(StandardCharsets.UTF_8);
                ByteBuf content = tailCtx.alloc().buffer(bytes.length).writeBytes(bytes);
                fullHttpResponse = new DefaultFullHttpResponse(response.version(), response.status(), content, response.headers(), EmptyHttpHeaders.INSTANCE);
                fullHttpResponse.headers()
                        .set(HttpHeaderNames.CONTENT_LENGTH, content.readableBytes());
                if (!fullHttpResponse.headers().contains(HttpHeaderNames.CONTENT_TYPE)){
                    fullHttpResponse.headers()
                            .set(HttpHeaderNames.CONTENT_TYPE, STRING_CONTENT_TYPE_PREFIX + DEFAULT_CHARSET);
                }
                break;
            }
            case BUF:
            {
                fullHttpResponse = new DefaultFullHttpResponse(response.version(), response.status(), response.content(), response.headers(), EmptyHttpHeaders.INSTANCE);
                fullHttpResponse.headers()
                        .set(HttpHeaderNames.CONTENT_LENGTH, response.content().readableBytes());
                if (!fullHttpResponse.headers().contains(HttpHeaderNames.CONTENT_TYPE)){
                    fullHttpResponse.headers()
                            .set(HttpHeaderNames.CONTENT_TYPE, BYTES_CONTENT_TYPE);
                }
                break;
            }
            case COMPLEX_OBJECT_FROM_READ:
            {
                fullHttpResponse = httpObjectServerResolver.resolverResponseOut(response, this);
                if (fullHttpResponse == null) {
                    throw new ResponseException("Provider httpObjectClientResolver set error, can not resolver this out response.");
                }
                break;
            }
            default:
            {
                fullHttpResponse = new DefaultFullHttpResponse(response.version(), response.status(), Unpooled.buffer(0), response.headers(), EmptyHttpHeaders.INSTANCE);
                fullHttpResponse.headers().set(HttpHeaderNames.CONTENT_LENGTH, 0);
                break;
            }
        }
        tailCtx.writeAndFlush(fullHttpResponse,promise);
        return promise;
    }

    public void responseRead(ChannelHandlerContext ctx, FullHttpResponse fullResponse) throws Exception{
        HttpResponse response = null;
        try{
            response = httpObjectClientResolver.resolverResponseIn(fullResponse);
            if (response == null){
                response = new HttpResponse(fullResponse.headers(), fullResponse.protocolVersion(), fullResponse.status());
                response.replaceStatus(HttpResponseStatus.UNSUPPORTED_MEDIA_TYPE);
            }
            onResponseRead(response);
        }catch (Throwable throwable){
            if (response == null){
                response = new HttpResponse(fullResponse.headers(),fullResponse.protocolVersion(),HttpResponseStatus.UNPROCESSABLE_ENTITY);
            }else {
                response.replaceStatus(HttpResponseStatus.UNPROCESSABLE_ENTITY);
            }
            onReadError(response,throwable);
        }

    }


    public abstract void onResponseRead(HttpResponse response) throws Exception;
    //todo 创建requestResolver扩展请求的解析
    public void requestRead(ChannelHandlerContext ctx, FullHttpRequest fullRequest){
        HttpRequest request = null;
        try{
            request = httpObjectServerResolver.resolverRequestIn(fullRequest);
            if (request == null){
                HttpHeaders headers = fullRequest.headers();
                request = new HttpRequest(headers, fullRequest.protocolVersion(),fullRequest.method() , fullRequest.uri());
                headers.set(UNABLE_TO_PARSE_REQUEST,1);
            }else {
                QueryStringDecoder queryStringDecoder = new QueryStringDecoder(request.uri());
                request.setParameters(queryStringDecoder.parameters());
            }
            onRequestRead(request);
        }catch (Throwable throwable){
            if (request == null){
                request = new HttpRequest(fullRequest.protocolVersion(),fullRequest.method(),"");
                request.setHeaders(fullRequest.headers());
            }
            request.headers().add(ERROR_HEADER,1);
            onReadError(request,throwable);
        }
    }

    public abstract void onRequestRead(HttpRequest request) throws Exception;
    protected abstract void onReadError(HttpObject httpObject,Throwable throwable);
    public abstract void setConnection(Connection connection);


    public void setHttpObjectServerResolver(HttpObjectServerResolver httpObjectServerResolver) {
        this.httpObjectServerResolver = httpObjectServerResolver;
    }
    public void setHttpObjectClientResolver(HttpObjectClientResolver httpObjectClientResolver){
        this.httpObjectClientResolver = httpObjectClientResolver;
    }
}
